package cz.cuni.amis.pogamut.episodic.query;

import cz.cuni.amis.pogamut.episodic.decisions.Node;
import cz.cuni.amis.pogamut.episodic.episodes.Chronobag;
import cz.cuni.amis.pogamut.episodic.episodes.Episode;
import cz.cuni.amis.pogamut.episodic.episodes.EpisodeNode;
import cz.cuni.amis.pogamut.episodic.episodes.ObjectNode;
import cz.cuni.amis.pogamut.episodic.episodes.ObjectSlot;
import cz.cuni.amis.pogamut.episodic.memory.AgentMemory;
import cz.cuni.amis.pogamut.episodic.memory.Parameters;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaCounter;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaEpisodeNode;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaSlot;
import cz.cuni.amis.pogamut.episodic.schemas.SlotContent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
 *
 * @author Miso
 */
public class QueryExecutor {
    private final AgentMemory mem;

    public QueryExecutor (AgentMemory mem) {
        this.mem = mem;
    }

    public String executeQuery(ComboTexts.ComboType type, boolean schemaOnly, String str) {
        String result = "Problem executing query.";
        mem.sem.acquireUninterruptibly();

        if (type == ComboTexts.ComboType.COMBO_ACTIVITY && schemaOnly) {
            result = executeActivitySchemaQuery(str);
        }

        if (type == ComboTexts.ComboType.COMBO_ACTIVITY && !schemaOnly) {
            result = executeActivityQuery(str);
        }

        if (type == ComboTexts.ComboType.COMBO_EPISODE && !schemaOnly) {
            str = str.substring(0, str.indexOf(" "));
            result = executeEpisodeQuery(Integer.parseInt(str));
        }

        mem.sem.release();
        return result;
    }

    private String executeEpisodeQuery(int idEpisode) {
        String str;
        Episode ep = null;
        ArrayList<Episode> eps = new ArrayList<Episode>();

        for (Chronobag ch : mem.chrono.getChronobags()) {
            for (Episode e : ch.getEpisodes()) {
                if (idEpisode == e.idEpisode) {
                    if (ep == null) {
                        ep = e;
                    } else
                    {
                        eps.add(e);
                    }
                }
            }
        }

        if (ep == null) {
            return "Episode not found.";
        }
        ep = ep.createCopy(null);
        for (Episode e : eps) {
            ep.mergeWith(e);
        }
        ep = ep.deriveEpisode(0, 0, -1);

        EpisodeNode root = ep.getRoot();
        if (root == null) return "Episode " + idEpisode + "deleted.";

        str = "Episode " + idEpisode + ":" + System.getProperty("line.separator");
        str += describeNode(root,0,idEpisode);

        return str;
    }

    private String executeActivitySchemaQuery(String name) {
        String str = "";
        Node node = mem.getDecisionTree().topLevelGoals.get(name);
        SchemaEpisodeNode schemaNode;
        ArrayList<SlotContent> contents = new ArrayList<SlotContent>();

        schemaNode = mem.schemas.getSchemaENode(node.getName());
        if (schemaNode == null) {
            return "Don't know.";
        }
        SchemaSlot slot = schemaNode.getSlot("Day");
        contents.addAll(slot.getSlotContents());
        int sum = 0;
        for (SlotContent content : contents) {
            sum += content.getCounts().get(0).iterator().next().getCount();
        }
        int count;
        for (SlotContent content : contents) {
             count = content.getCounts().get(0).iterator().next().getCount();
             if (count * 10 >= sum) {
                 str += content.getObject().getName() + ": ";
                 str += count + "/" + sum + " = ";
                 str += 100*count/sum + "%." + System.getProperty("line.separator");
             }
        }

        return str;
    }

    private String describeNode(EpisodeNode node, int offset, int idEpisode) {
        if (node.getAssociatedNode() == null) {
            return "";
        }
        String spaces = "";
        for (int i = 0; i < offset; i++) {
            spaces += " ";
        }
        String str = spaces + "Performed " + node.getName() + "." + System.getProperty("line.separator");

        String slotStr;
        for (SchemaSlot slot : node.getAssociatedNode().getAssociatedNode().getSlots()) {
            slotStr = describeSlot(node, slot.getType());
            str += spaces + slotStr + System.getProperty("line.separator");
        }

        Collection<EpisodeNode> children = node.getChildrenNodes();
        Collection<Node> childrenNodes = new HashSet<Node>();
        for (EpisodeNode n : children) {
            childrenNodes.add(n.getAssociatedNode());
        }

        str += spaces + "Performed by doing: ";
        Collection<Node> allchildrenNodes = node.getAssociatedNode().getAllChildrenNodes();

        int totalCount = 0;
        Collection<SchemaEpisodeNode> childrenSchemaNodes = new ArrayList<SchemaEpisodeNode>();
        for (Node n : allchildrenNodes) {
            if (n.getAssociatedNode() != null) {
                childrenSchemaNodes.add(n.getAssociatedNode());
            }
            if (n.getAssociatedNode() != null) {
                totalCount += n.getAssociatedNode().getSingleCount(0, 0);
            }
        }

        double perc;
        Collection<SchemaCounter> counters;
        for (SchemaEpisodeNode n : childrenSchemaNodes) {
            perc = 0;
            counters = n.getCounts().get(node.getAssociatedNode().getAssociatedNode().getId());
            for (SchemaCounter c : counters) {
                if (c.containsNode(node.getAssociatedNode().getAssociatedNode().getId())) {
                    perc += c.getCount() / totalCount * (1 - mem.parameters.EPISODE_TREE_RELIANCE);
                }
            }
            if (childrenNodes.contains(n.getAssociatedNode())) {
                perc += mem.parameters.EPISODE_TREE_RELIANCE;
            }

            str += n.getName() + "(" + Math.round(perc*100) + "%); ";
        }
        str += System.getProperty("line.separator");

        EpisodeNode subnode = node.getFirstChild().get(idEpisode);
        while (subnode != null) {
            if (subnode.getAssociatedNode() != null) {
                str += describeNode(subnode, offset + 2, idEpisode);
            }
            subnode = subnode.getSuccessor().get(idEpisode);
        }

        return str;
    }

    private String describeSlot(EpisodeNode node, String slotName) {
        ObjectSlot slot = node.getObjectSlot(slotName);
        String slotStr = slotName + ": ";
        Collection<ObjectNode> obj;
        if (slot != null) {
            obj = slot.getUsedObjects();
        } else {
            obj = new ArrayList<ObjectNode>();
        }
        int objects = obj.size();
        SchemaSlot schemaSlot = node.getAssociatedNode().getAssociatedNode().getSlot(slotName);
        if (schemaSlot == null) return slotStr + "unknown";

        ArrayList<SlotContent> contents = new ArrayList<SlotContent>();
        contents.addAll(schemaSlot.getSlotContents());
        int sum = 0;
        for (SlotContent content : contents) {
            sum += content.getCounts().get(0).iterator().next().getCount();
        }
        int count;
        double percentage;
        for (SlotContent content : contents) {
             count = content.getCounts().get(0).iterator().next().getCount();
             if (objects > 0) {
                percentage = 100*count/sum * (1 - mem.parameters.EPISODE_TREE_RELIANCE);
             } else {
                percentage = 100*count/sum;
             }
             for (ObjectNode o : obj) {
                 if (o.getName().equals(content.getObject().getName())) {
                     percentage += 100 * mem.parameters.EPISODE_TREE_RELIANCE / objects;
                 }
             }
             slotStr += content.getObject().getName() + " - " + Math.round(percentage) + "%; ";
        }
        if (objects == 0) {
            slotStr += "(guess)";
        }

        return slotStr;
    }

    private String executeActivityQuery(String name) {
        //When did you last execute top level goal 'name'?
        String str = "";

        ArrayList<String> days = new ArrayList<String>();
        Integer minday = null;
        Integer maxday = null;
        for (int level = 0; level < mem.parameters.MAX_CHRONOBAG_LEVELS; level++) {
            Chronobag c = mem.chrono.getChronobagSequenceEnds().get(level);
            while (c != null) {
                for (Episode e : c.getEpisodes()) {
                    if (e.getRoot().getName().equals(name)) {
                        if (maxday == null || maxday > c.end) {
                            minday = c.begin;
                            maxday = c.end;

                            ObjectSlot s = e.getRoot().getObjectSlot("Day");
                            if (s != null && !s.getUsedObjects().isEmpty()) {
                                days.clear();
                                for (ObjectNode o : s.getUsedObjects()) {
                                    days.add(o.getName());
                                }
                            }
                        }

                    }
                }
                c = c.older;
            }
        }

        if (minday != null && maxday != null) {
            str += minday + " - " + maxday + " day(s) ago. ";
            str += "Possible days: " + days;
        }

        //day of week
     /*   Node node = mem.getDecisionTree().topLevelGoals.get(name);
        SchemaEpisodeNode schemaNode;
        ArrayList<SlotContent> contents = new ArrayList<SlotContent>();

        schemaNode = mem.getSchemaBag().getSchemaENode(node.getName());
        if (schemaNode == null) {
            return "Don't know.";
        }
        SchemaSlot slot = schemaNode.getSlot("Day");
        contents.addAll(slot.getSlotContents());
        int sum = 0;
        for (SlotContent content : contents) {
            sum += content.getCounts().get(0).iterator().next().getCount();
        }
        int count;
        for (SlotContent content : contents) {
             count = content.getCounts().get(0).iterator().next().getCount();
             if (count * 10 >= sum) {
                 str += content.getObject().getName() + ": ";
                 str += count + "/" + sum + " = ";
                 str += 100*count/sum + "%." + System.getProperty("line.separator");
             }
        }
*/
        if (str.equals("")) {
            str = "Don't remember.";
        }
        return str;
    }

    public ArrayList<String> executeRoomSchemaQuery() {
        ArrayList<String> res = new ArrayList<String>();
        Chronobag c = mem.chrono.getPresentChronobag();
        Episode epis = null;
        while (c != null) {
            for (Episode e : c.getEpisodes()) {
                    epis = e;
                    break;
            }
            if (epis != null) break;
            c = c.older;
        }
        epis = epis.deriveEpisode(0, 0, -1);

        EpisodeNode n = epis.getRoot();
        if (n == null) return res;
        n = n.getChild("AObserve");
        if (n == null) return res;
        ObjectSlot s = n.getObjectSlot("perceived");
        if (s == null) return res;

        for (ObjectNode o : s.getUsedObjects()) {
            res.add(o.getName());
           // res.add(o.getName() + " " + s.getScore(o));
        }

        return res;
    }

    /**
     * Returns number of nodes remembered for each episode specified in parameter.
     * Merges episode copies from all chronobag levels and derives them before
     * calculating the number of nodes.
     *
     * @param episodeIDs    Set of queried episode IDs
     * @return  Returns a map with number of nodes mapped to individual episode IDs
     */
    public Map<Integer, Integer> executeForgettingCurveQuery(Collection<Integer> episodeIDs) {
        Map<Integer, Integer> result = new HashMap<Integer, Integer>();
        for (Integer id : episodeIDs) {
            result.put(id, 0);
            Collection<Episode> episodes = new HashSet<Episode>();
            for (Chronobag endCh : mem.chrono.getChronobagSequenceEnds().values()) {
                //TODO delete this to receive also episodes from abstract chronobags
                if (endCh != mem.chrono.getPresentChronobag()) continue;
                Chronobag c = endCh;
                while (c != null) {
                    for (Episode e : c.getEpisodes()) {
                        if (e.getIdEpisode() == id) {
                            episodes.add(e.createCopy(null));
                        }
                    }
                    c = c.older;
                }
            }
            if (episodes.isEmpty()) continue;
            if (episodes.size() > 1) {
                //TODO merge episodes
            }
            Episode e = episodes.iterator().next();
            //TODO need to work with derived episodes -> uncomment below
            //e = e.deriveEpisode();
            result.put(id, e.getRoot().getNumberOfSubNodesWithObjects() + 1);
        }
        return result;
    }

    /**
     * Returns number of nodes that exist in one episode but do not exist in another episode.
     * The two episodes do not have to be in same memory class.
     *
     * @param id1   ID of episode 1 (in this memory class)
     * @param mem2  Memory class containing episode 2
     * @param id2   ID of episode 2
     * @param derive1    When set to true, episode 1 will be derived from schema
     * @param derive2    When set to true, episode 2 will be derived from schema
     * @return  Returns number of nodes that exist in episode2 but do not exist in episode 1
     */
    public Integer getNumberOfForgottenNodes(Integer id1, AgentMemory mem2, Integer id2, boolean derive1, boolean derive2) {
        Episode epis1 = null;
        for (Chronobag c : mem.chrono.getChronobags()) {
            for (Episode e : c.getEpisodes()) {
                if (e.getIdEpisode() == id1) {
                    if (epis1 == null) {
                        epis1 = e.createCopy(c);
                    } else {
                        epis1.mergeWith(e);
                    }
                }
            }
        }
        Episode epis2 = null;
        for (Chronobag c : mem2.chrono.getChronobags()) {
            for (Episode e : c.getEpisodes()) {
                if (e.getIdEpisode() == id2) {
                    if (epis2 == null) {
                        epis2 = e.createCopy(c);
                    } else {
                        epis2.mergeWith(e);
                    }
                }
            }
        }

        //TODO: this is causing problems right now - debug
        if (epis1 != null) epis1 = epis1.deriveEpisode(0, 0, -1);
        if (epis2 != null) epis2 = epis2.deriveEpisode(0, 0, -1);

        if (epis1 != null) epis1.getRoot().recalculateTreeSize(false);
        if (epis2 != null) epis2.getRoot().recalculateTreeSize(false);

        if (epis2 == null) return 0;
        if (epis1 == null) return epis2.getRoot().getNumberOfSubNodes() + 1;

        assert (epis1.getRoot().getName().equals(epis2.getRoot().getName()));
        int epis1nodes = epis1.getRoot().getNumberOfSubNodes();
        Episode merged = epis1;
        merged.mergeWith(epis2);
    //    if (derive2) System.out.print(merged.getRoot().getNumberOfSubNodes() - epis1nodes);
    //    if (derive2) System.out.println(" " + merged.getRoot().getName());
        //TODO: use subnodesWithObjects instead
        return merged.getRoot().getNumberOfSubNodes() - epis1nodes;
    }
}
